Detecting stops¶

Binder IPYNB HTML

There are no definitive answers when it comes to detecting / extracting stops from movement trajectories. Due to tracking inaccuracies, movement speed rarely goes to true zero. GPS tracks, for example, tend to keep moving around the object's stop location.

Suitable stop definitions are also highly application dependent. For example, an application may be interested in analyzing trip purposes. To do so, analysts would be interested in stops that are longer than, for example, 5 minutes and may try to infer the purpose of the stop from the stop location and time. Shorter stops, such as delays at traffic lights, however would not be relevant for this appication.

In the MovingPandas TrajectoryStopDetector implementation, a stop is detected if the movement stays within an area of specified size for at least the specified duration.

In [1]:
import pandas as pd
import geopandas as gpd
from geopandas import GeoDataFrame, read_file
from shapely.geometry import Point, LineString, Polygon
from datetime import datetime, timedelta
import movingpandas as mpd

import warnings
warnings.filterwarnings('ignore')

mpd.show_versions()
MovingPandas 0.9.rc2

SYSTEM INFO
-----------
python     : 3.7.12 | packaged by conda-forge | (default, Oct 26 2021, 05:37:49) [MSC v.1916 64 bit (AMD64)]
executable : E:\Anaconda\envs\mpd-ex\python.exe
machine    : Windows-10-10.0.19041-SP0

GEOS, GDAL, PROJ INFO
---------------------
GEOS       : None
GEOS lib   : None
GDAL       : 3.2.1
GDAL data dir: None
PROJ       : 7.2.0
PROJ data dir: E:\Anaconda\envs\mpd-ex\Library\share\proj

PYTHON DEPENDENCIES
-------------------
geopandas  : 0.10.2
pandas     : 1.3.5
fiona      : 1.8.18
numpy      : 1.21.5
shapely    : 1.7.1
rtree      : 0.9.7
pyproj     : 3.1.0
matplotlib : 3.5.1
mapclassify: 2.4.3
geopy      : 2.2.0
holoviews  : 1.14.6
hvplot     : 0.7.3
geoviews   : 1.9.2
In [2]:
FSIZE = 350
In [3]:
gdf = read_file('../data/geolife_small.gpkg')
traj_collection = mpd.TrajectoryCollection(gdf, 'trajectory_id', t='t')

Stop detection with a single Trajectory¶

In [4]:
my_traj = traj_collection.trajectories[0]
my_traj
Out[4]:
Trajectory 1 (2008-12-11 04:42:14 to 2008-12-11 05:15:46) | Size: 466 | Length: 6207.0m
Bounds: (116.385602, 39.862378, 116.393553, 39.898723)
LINESTRING (116.391305 39.898573, 116.391317 39.898617, 116.390928 39.898613, 116.390833 39.898635, 
In [5]:
traj_plot = my_traj.hvplot(title='Trajectory {}'.format(my_traj.id), line_width=7.0, tiles='CartoLight', color='slategray', frame_width=FSIZE, frame_height=FSIZE) 
traj_plot
Out[5]:
In [6]:
detector = mpd.TrajectoryStopDetector(my_traj)

Stop duration¶

In [7]:
%%time
stop_time_ranges = detector.get_stop_time_ranges(min_duration=timedelta(seconds=60), max_diameter=100)
Wall time: 1.25 s
In [8]:
for x in stop_time_ranges: 
    print(x)
Traj 1: 2008-12-11 04:42:14 - 2008-12-11 04:43:32 (duration: 0 days 00:01:18)
Traj 1: 2008-12-11 04:43:50 - 2008-12-11 04:47:48 (duration: 0 days 00:03:58)
Traj 1: 2008-12-11 04:50:06 - 2008-12-11 04:51:24 (duration: 0 days 00:01:18)
Traj 1: 2008-12-11 04:54:50 - 2008-12-11 04:55:54 (duration: 0 days 00:01:04)
Traj 1: 2008-12-11 05:02:03 - 2008-12-11 05:06:40 (duration: 0 days 00:04:37)
Traj 1: 2008-12-11 05:07:19 - 2008-12-11 05:08:31 (duration: 0 days 00:01:12)
Traj 1: 2008-12-11 05:11:17 - 2008-12-11 05:14:43 (duration: 0 days 00:03:26)

Stop points¶

In [9]:
help(mpd.TrajectoryStopDetector.get_stop_points)
Help on function get_stop_points in module movingpandas.trajectory_stop_detector:

get_stop_points(self, max_diameter, min_duration)
    Returns detected stop location points
    
    Parameters
    ----------
    max_diameter : float
        Maximum diameter for stop detection
    min_duration : datetime.timedelta
        Minimum stop duration
    
    Returns
    -------
    geopandas.GeoDataFrame
        Stop locations as points with start and end time and stop duration
        in seconds
    
    Examples
    --------
    
    >>> detector = mpd.TrajectoryStopDetector(traj)
    >>> stops = detector.get_stop_points(min_duration=timedelta(seconds=60),
                                         max_diameter=100)

In [10]:
%%time
stop_points = detector.get_stop_points(min_duration=timedelta(seconds=60), max_diameter=100)
Wall time: 1.19 s
In [11]:
stop_points
Out[11]:
geometry start_time end_time traj_id duration_s
stop_id
1_2008-12-11 04:42:14 POINT (116.39131 39.89857) 2008-12-11 04:42:14 2008-12-11 04:43:32 1 78.0
1_2008-12-11 04:43:50 POINT (116.39052 39.89793) 2008-12-11 04:43:50 2008-12-11 04:47:48 1 238.0
1_2008-12-11 04:50:06 POINT (116.38898 39.88945) 2008-12-11 04:50:06 2008-12-11 04:51:24 1 78.0
1_2008-12-11 04:54:50 POINT (116.39256 39.87882) 2008-12-11 04:54:50 2008-12-11 04:55:54 1 64.0
1_2008-12-11 05:02:03 POINT (116.39314 39.86340) 2008-12-11 05:02:03 2008-12-11 05:06:40 1 277.0
1_2008-12-11 05:07:19 POINT (116.39084 39.86382) 2008-12-11 05:07:19 2008-12-11 05:08:31 1 72.0
1_2008-12-11 05:11:17 POINT (116.38662 39.86523) 2008-12-11 05:11:17 2008-12-11 05:14:43 1 206.0
In [12]:
stop_point_plot = traj_plot * stop_points.hvplot(geo=True, size='duration_s', color='deeppink')
stop_point_plot
Out[12]:

Stop segments¶

In [13]:
help(mpd.TrajectoryStopDetector.get_stop_segments)
Help on function get_stop_segments in module movingpandas.trajectory_stop_detector:

get_stop_segments(self, max_diameter, min_duration)
    Returns detected stop trajectory segments
    
    Parameters
    ----------
    max_diameter : float
        Maximum diameter for stop detection
    min_duration : datetime.timedelta
        Minimum stop duration
    
    Returns
    -------
    TrajectoryCollection
        Trajectory segments
    
    Examples
    --------
    
    >>> detector = mpd.TrajectoryStopDetector(traj)
    >>> stops = detector.get_stop_segments(min_duration=timedelta(seconds=60),
                                           max_diameter=100)

In [14]:
%%time
stops = detector.get_stop_segments(min_duration=timedelta(seconds=60), max_diameter=100)
Wall time: 1.01 s
In [15]:
stops
Out[15]:
TrajectoryCollection with 7 trajectories
In [16]:
stop_segment_plot = stop_point_plot * stops.hvplot( size=200, line_width=7.0, tiles=None, color='orange') 
stop_segment_plot
Out[16]:

Split at stops¶

In [17]:
help(mpd.StopSplitter)
Help on class StopSplitter in module movingpandas.trajectory_splitter:

class StopSplitter(TrajectorySplitter)
 |  StopSplitter(traj)
 |  
 |  Split trajectories at detected stops.
 |  A stop is detected if the movement stays within an area of specified size for
 |  at least the specified duration.
 |  
 |  Parameters
 |  ----------
 |  max_diameter : float
 |      Maximum diameter for stop detection
 |  min_duration : datetime.timedelta
 |      Minimum stop duration
 |  min_length : numeric
 |      Desired minimum length of trajectories. Shorter trajectories are discarded.
 |      (Length is calculated using CRS units, except if the CRS is geographic
 |      (e.g. EPSG:4326 WGS84) then length is calculated in metres.)
 |  
 |  Examples
 |  --------
 |  
 |  >>> mpd.StopSplitter(traj).split(max_diameter=7, min_duration=timedelta(seconds=60))
 |  
 |  Method resolution order:
 |      StopSplitter
 |      TrajectorySplitter
 |      builtins.object
 |  
 |  Static methods defined here:
 |  
 |  get_time_ranges_between_stops(traj, stop_ranges)
 |  
 |  ----------------------------------------------------------------------
 |  Methods inherited from TrajectorySplitter:
 |  
 |  __init__(self, traj)
 |      Create TrajectoryGeneralizer
 |      
 |      Parameters
 |      ----------
 |      traj : Trajectory or TrajectoryCollection
 |  
 |  split(self, **kwargs)
 |      Split the input Trajectory/TrajectoryCollection.
 |      
 |      Parameters
 |      ----------
 |      kwargs : any type
 |          Split parameters, differs by splitter
 |      
 |      Returns
 |      -------
 |      TrajectoryCollection
 |          Split trajectories
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from TrajectorySplitter:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

In [18]:
%%time
split = mpd.StopSplitter(my_traj).split(min_duration=timedelta(seconds=60), max_diameter=100)
Wall time: 1.16 s
In [19]:
split
Out[19]:
TrajectoryCollection with 7 trajectories
In [20]:
split.to_traj_gdf()
Out[20]:
traj_id start_t end_t geometry length direction
0 1_2008-12-11 04:43:32 2008-12-11 04:43:32 2008-12-11 04:43:50 LINESTRING (116.39083 39.89863, 116.38941 39.8... 251.838254 198.648318
1 1_2008-12-11 04:47:48 2008-12-11 04:47:48 2008-12-11 04:50:06 LINESTRING (116.38984 39.89824, 116.38974 39.8... 1032.611896 184.305030
2 1_2008-12-11 04:51:24 2008-12-11 04:51:24 2008-12-11 04:54:50 LINESTRING (116.39010 39.88925, 116.39027 39.8... 1369.652684 169.759596
3 1_2008-12-11 04:55:54 2008-12-11 04:55:54 2008-12-11 05:02:03 LINESTRING (116.39189 39.87821, 116.39183 39.8... 1918.855972 176.302880
4 1_2008-12-11 05:06:40 2008-12-11 05:06:40 2008-12-11 05:07:19 LINESTRING (116.39227 39.86397, 116.39217 39.8... 124.917120 262.140310
5 1_2008-12-11 05:08:31 2008-12-11 05:08:31 2008-12-11 05:11:17 LINESTRING (116.38974 39.86382, 116.38967 39.8... 439.686682 300.319621
6 1_2008-12-11 05:14:43 2008-12-11 05:14:43 2008-12-11 05:15:46 LINESTRING (116.38591 39.86545, 116.38603 39.8... 84.267811 133.034126
In [21]:
stop_segment_plot + split.hvplot(title='Trajectory {} split at stops'.format(my_traj.id), line_width=7.0, tiles='CartoLight', frame_width=FSIZE, frame_height=FSIZE)
Out[21]:

Stop Detection for TrajectoryCollections¶

The process is the same as for individual trajectories.

In [22]:
%%time
detector = mpd.TrajectoryStopDetector(traj_collection)
stop_points = detector.get_stop_points(min_duration=timedelta(seconds=120), max_diameter=100)
len(stop_points)
Wall time: 13.9 s
Out[22]:
26
In [23]:
ax = traj_collection.plot(figsize=(7,7))
stop_points.plot(ax=ax, color='red',  )
Out[23]:
<AxesSubplot:>
In [ ]: